#pragma once

#include "Macros.h"
#include <atomic>
#include <securitybaseapi.h>
#include <type_traits>
#include <utility>
#include <cstdint>

namespace SL
{
/**
 * @brief 
 @a Intrusive ``shared_ptr`` / COM
 *  ``ref`` + ``Object`` = ``shared_ptr``
 *  ``ref``    : a wrapper that manages incRef & decRef
 *  ``Object`` : a counter block & base class
 - Intrusive smart pointers allow reattaching to raw_ptr (std::enable_shared_from_this ?)
 */
class SELAH_API Object
{
public:
    Object() = default;
    Object(const Object&) {}
    Object& operator=(const Object&) { return *this; }
    Object(Object&&) = delete;
    Object& operator=(Object&&) = delete;

    virtual ~Object() = default;

    /// Return the name of the class.
    virtual const char* getClassName() const { return "Object"; }

    int refCount() const { return mRefCount; }

    void incRef() const;

    void decRef(bool dealloc = true) const noexcept;

private:
    mutable std::atomic<uint32_t> mRefCount{0};
};

/// Macro to declare the object class name.
#define SELAH_OBJECT(class_)                  \
public:                                       \
    const char* getClassName() const override \
    {                                         \
        return #class_;                       \
    }

/**
 * @brief Reference counting helper.
 *
 * The @a ref template is a simple wrapper to store a pointer to an object. It
 * takes care of increasing and decreasing the object's reference count as
 * needed. When the last reference goes out of scope, the associated object
 * will be deallocated.
 *
 * This class follows similar semantics to the ``std::shared_ptr`` class from
 * the STL. In particular, we avoid implicit conversion to and from raw
 * pointers.
 */
template<typename T>
class ref
{
public:
    /// Default constructor (nullptr).
    ref() {}

    /// Construct a reference from a nullptr.
    ref(std::nullptr_t) {}

    /// Construct a reference from a convertible pointer.
    template<typename T2 = T>
    explicit ref(T2* ptr) : mPtr(ptr)
    {
        static_assert(std::is_base_of_v<Object, T2>, "Cannot create reference to object not inheriting from Object class.");
        static_assert(std::is_convertible_v<T2*, T*>, "Cannot create reference to object from unconvertible pointer type.");
        if (mPtr)
            incRef((const Object*)(mPtr));
    }

    /// Copy constructor.
    ref(const ref& r) : mPtr(r.mPtr)
    {
        if (mPtr)
            incRef((const Object*)(mPtr));
    }

    /// Construct a reference from a convertible reference.
    template<typename T2 = T>
    ref(const ref<T2>& r) : mPtr(r.mPtr)
    {
        static_assert(std::is_base_of_v<Object, T>, "Cannot create reference to object not inheriting from Object class.");
        static_assert(std::is_convertible_v<T2*, T*>, "Cannot create reference to object from unconvertible reference.");
        if (mPtr)
            incRef((const Object*)(mPtr));
    }

    /// Move constructor.
    ref(ref&& r) noexcept : mPtr(r.mPtr) { r.mPtr = nullptr; }

    /// Construct a reference by moving from a convertible reference.
    template<typename T2>
    ref(ref<T2>&& r) noexcept : mPtr(r.mPtr)
    {
        static_assert(std::is_base_of_v<Object, T>, "Cannot create reference to object not inheriting from Object class.");
        static_assert(std::is_convertible_v<T2*, T*>, "Cannot create reference to object from unconvertible reference.");
        r.mPtr = nullptr;
    }

    /// Destructor.
    ~ref()
    {
        if (mPtr)
            decRef((const Object*)(mPtr));
    }

    /// Assign another reference into the current one.
    ref& operator=(const ref& r) noexcept
    {
        if (r != *this)
        {
            if (r.mPtr)
                incRef((const Object*)(r.mPtr));
            T* prevPtr = mPtr;
            mPtr = r.mPtr;
            if (prevPtr)
                decRef((const Object*)(prevPtr));
        }
        return *this;
    }

    /// Assign another convertible reference into the current one.
    template<typename T2>
    ref& operator=(const ref<T2>& r) noexcept
    {
        static_assert(std::is_convertible_v<T2*, T*>, "Cannot assign reference to object from unconvertible reference.");
        if (r != *this)
        {
            if (r.mPtr)
                incRef((const Object*)(r.mPtr));
            T* prevPtr = mPtr;
            mPtr = r.mPtr;
            if (prevPtr)
                decRef((const Object*)(prevPtr));
        }
        return *this;
    }

    /// Move another reference into the current one.
    ref& operator=(ref&& r) noexcept
    {
        /**
         * @brief Using static_cast<void*> ensures that the comparison is performed at the address level, not influenced by any other
         * potential overloaded operators or type-specific behaviors.
         */
        if (static_cast<void*>(&r) != this)
        {
            if (mPtr)
                decRef((const Object*)(mPtr));
            mPtr = r.mPtr;
            r.mPtr = nullptr;
        }
        return *this;
    }

    /// Move another convertible reference into the current one.
    template<typename T2>
    ref& operator=(ref<T2>&& r) noexcept
    {
        static_assert(std::is_convertible_v<T2*, T*>, "Cannot move reference to object from unconvertible reference.");
        if (static_cast<void*>(&r) != this)
        {
            if (mPtr)
                decRef((const Object*)(mPtr));
            mPtr = r.mPtr;
            r.mPtr = nullptr;
        }
        return *this;
    }

    /// Overwrite this reference with a pointer to another object
    template<typename T2 = T>
    void reset(T2* ptr = nullptr) noexcept
    {
        static_assert(std::is_convertible_v<T2*, T*>, "Cannot assign reference to object from unconvertible pointer.");
        if (ptr != mPtr)
        {
            if (ptr)
                incRef((const Object*)(ptr));
            T* prevPtr = mPtr;
            mPtr = ptr;
            if (prevPtr)
                decRef((const Object*)(prevPtr));
        }
    }

    /// Compare this reference to another reference.
    template<typename T2 = T>
    bool operator==(const ref<T2>& r) const
    {
        static_assert(
            std::is_convertible_v<T2*, T*> || std::is_convertible_v<T*, T2*>, "Cannot compare references of non-convertible types."
        );
        return mPtr == r.mPtr;
    }

    /// Compare this reference to another reference.
    template<typename T2 = T>
    bool operator!=(const ref<T2>& r) const
    {
        static_assert(
            std::is_convertible_v<T2*, T*> || std::is_convertible_v<T*, T2*>, "Cannot compare references of non-convertible types."
        );
        return mPtr != r.mPtr;
    }

    /// Compare this reference to another reference.
    template<typename T2 = T>
    bool operator<(const ref<T2>& r) const
    {
        static_assert(
            std::is_convertible_v<T2*, T*> || std::is_convertible_v<T*, T2*>, "Cannot compare references of non-convertible types."
        );
        return mPtr < r.mPtr;
    }

    /// Compare this reference to a pointer.
    template<typename T2 = T>
    bool operator==(const T2* ptr) const
    {
        static_assert(std::is_convertible_v<T2*, T*>, "Cannot compare reference to pointer of non-convertible types.");
        return mPtr == ptr;
    }

    /// Compare this reference to a pointer.
    template<typename T2 = T>
    bool operator!=(const T2* ptr) const
    {
        static_assert(std::is_convertible_v<T2*, T*>, "Cannot compare reference to pointer of non-convertible types.");
        return mPtr != ptr;
    }

    /// Compare this reference to a null pointer.
    bool operator==(std::nullptr_t) const { return mPtr == nullptr; }

    /// Compare this reference to a null pointer.
    bool operator!=(std::nullptr_t) const { return mPtr != nullptr; }

    /// Compare this reference to a null pointer.
    bool operator<(std::nullptr_t) const { return mPtr < nullptr; }

    /// Access the object referenced by this reference.
    T* operator->() const { return mPtr; }

    /// Return a C++ reference to the referenced object.
    T& operator*() const { return *mPtr; }

    /// Return a pointer to the referenced object.
    T* get() const { return mPtr; }

    /// Check if the object is defined
    operator bool() const { return mPtr != nullptr; }

    /// Swap this reference with another reference.
    void swap(ref& r) noexcept
    {
        std::swap(mPtr, r.mPtr);
    }

private:
    inline void incRef(const Object* object)
    {
        object->incRef();
    }

    inline void decRef(const Object* object)
    {
        object->decRef();
    }

    T* mPtr{nullptr};

private:

    template<typename T2>
    friend class ref;
};

} // namespace SL
